ESTIMACIÓN DEL PRECIO DE UN VEHÍCULO

Valencian Telecommunications, SA (VALTEL) es una compañía prestadora de servicios de telecomunicaciones que se ha especializado en ciudadanos no nacionales residentes en España. Su propuesta de valor es ofrecer packs económicos focalizados en las llamadas internacionales y la itinerancia de datos (roaming) y tarifas flexibles de datos.

En su plan estratégico, se contempla la diversificación de servicios y se plantea el diseño de nuevas áreas de la compañía dedicadas a comercializar a través de internet bienes de interés para el colectivo de ciudadanos no nacionales. Por medio de encuestas a sus clientes, se ha detectado que la compra de vehículos para ciudadanos no nacionales puede ser un potencial nicho, y, concretamente, el mercado de coches de segunda mano.

VALTEL desea ofertar dentro de su app móvil un servicio que permita a sus clientes buscar vehículos en venta y ofrecerles un precio competitivo. Para conocer dicho precio, el área de I+D ha conseguido un dataset (Coches_Segunda_Mano.csv) con las ventas de vehículos de segunda mano y encarga al área de Ciencia de Datos la estimación del precio de cada vehículo para poder elaborar una tarifa competitiva.

Por tanto, el trabajo para realizar será el siguiente:

1) Utilizar la metodología CRISP-DM para estimar el precio de un vehículo (campo Precio). Se recomienda utilizar como base el cuaderno del caso de repaso o bien BigML. 2) Realizar una narrativa que comunique los hallazgos. Por simplicidad, se recomienda utilizar PowerPoint o equivalente para realizar la narrativa.

In [1]:
import warnings
warnings.filterwarnings('ignore')
In [2]:
# Librerías
import numpy as np
import pandas as pd
from statistics import mode
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import pearsonr
from sklearn.neighbors import LocalOutlierFactor
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, mean_squared_error, r2_score
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import xgboost as xgb
from sklearn.linear_model import Ridge
from xgboost import XGBRegressor
import scipy.stats as stats
In [3]:
## FUNCIONES ##

# Devuelve el mínimo y máximo de una variable dada
def var_min_max(_df, _var):
    return (_df[_var].min(), _df[_var].max())


# Devuelve un dataframe con información de análisis univariante de variables cualitativas:
def univar_categ(_df, _list_var):
    univar_data = []
    for _var in _list_var:
        univar_data.append([_var, _df[_var].nunique(), mode(_df[_var])])
    return pd.DataFrame(univar_data, columns=["Variable", "Valores Únicos", "Moda"])
    

# Devuelve un dataframe con información de análisis univariante de variables cuantitativas continuas:
def univar_num_cont(_df, _list_var):
    univar_data = []
    for _var in _list_var:
        univar_data.append([_var, _df[_var].nunique(), round(_df[_var].mean(),2), _df[_var].median(), mode(_df[_var]), 
                            _df[_var].min(), _df[_var].max()])
    return pd.DataFrame(univar_data, columns=["Variable", "Valores Únicos", "Media", "Mediana", "Moda", "Mínimo", "Máximo"])


# Devuelve un dataframe con información de análisis univariante de variables cuantitativas discretas:
def univar_num_disc(_df, _list_var):
    univar_data = []
    for _var in _list_var:
        univar_data.append([_var, _df[_var].nunique(), _df[_var].median(), mode(_df[_var]), 
                            _df[_var].min(), _df[_var].max()])
    return pd.DataFrame(univar_data, columns=["Variable", "Valores Únicos", "Mediana", "Moda", "Mínimo", "Máximo"])


# Rellena valores nulos de una variable con su moda
def impute_mode(_df, _var):
    return _df[_var].fillna(mode(_df[_var]))


# Para estandarizar el dataset original con el que se entrena y valida el modelo y posteriores datasets
def standard_dataset(_x, _var_list):
    scaler = StandardScaler()
    
    for var in _var_list:
        _x[var] = scaler.fit_transform(_x[[var]])

    return _x


# Cálculo del R2 ajustado
def adjusted_r2(_r2, _n, _p):
    return 1 - ((1 - _r2) * (_n - 1) / (_n - _p - 1))
In [4]:
## GRÁFICOS ##
# Representa un gráfico de barras para una variable cualitativa (máx. 20 categorías)
def univar_graph_categ(_df, _var):
    if _df[_var].nunique() > 20:
        cat = _df[_var].value_counts().head(20)
    else:
        cat = _df[_var].value_counts()

    plt.figure(figsize=(10, 6))
    cat.plot(kind='bar', color='skyblue')
    
    plt.title(f"Distribución de '{_var}'")
    plt.ylabel("Frecuencia")
    plt.xlabel(f"{_var}")
    plt.xticks(rotation=65)
    
    plt.show()
    
      
# Representa un gráfico de frecuencias y una línea de porcentaje de frecuencias acumuladas para una variable cualitativa
def univar_graph_categ_extra(_df, _var):
    frequencies = _df[_var].value_counts()
    cumulative_percent = np.cumsum(frequencies) / frequencies.sum() * 100

    fig, ax1 = plt.subplots(figsize=(12, 6))

    ax1.plot(frequencies.values, color='C0', marker='o')
    ax1.set_xlabel(f'{_var} (ordenados de mayor a menor)')
    ax1.set_ylabel('Frecuencia', color='C0')
    ax1.tick_params('y', colors='C0')
    ax1.set_xticks([]) 

    ax2 = ax1.twinx()
    ax2.plot(cumulative_percent, color='C1', marker='o', ms=5)
    ax2.set_ylabel('Porcentaje Acumulado (%)', color='C1')
    ax2.tick_params('y', colors='C1')

    plt.title('Distribución de la variable Modelo y Porcentaje Acumulado')

    plt.tight_layout()
    plt.show()


# Representa un histograma con líneas de moda, mediana y media para una variable cuantitativa continua
def univar_graph_num_cont(_df, _var, _bins=False):
    if _bins == False:
        _bins = 20
    
    media = _df[_var].mean()
    mediana = _df[_var].median()
    moda = mode(_df[_var])
    
    plt.figure(figsize=(10,6))
    plt.axvline(moda, color='red', linestyle='-', label=f'Moda: {moda:.2f}')
    plt.axvline(mediana, color='orange', linestyle='-', label=f'Mediana: {mediana:.2f}')
    plt.axvline(media, color='black', linestyle='-', label=f'Media: {media:.2f}')
    sns.histplot(_df[_var], bins=_bins, kde=True)
    
    plt.xlabel(_var)
    plt.ylabel("Frecuencia")
    plt.title(f"Histograma de '{_var}'")
    
    plt.legend()
    plt.show()
    
    
# Representa un gráfico de barras para una variable cuantitativa discreta
def univar_graph_num_disc(_df, _var):
    values = _df[_var].value_counts().sort_index()
    
    plt.figure(figsize=(10, 6))
    values.plot(kind='bar', color='skyblue')
    plt.title(f"Distribución de '{_var}'")
    plt.ylabel("Frecuencia")
    plt.xlabel(f"{_var}")
    plt.xticks(rotation=65)
    
    plt.show()
    
    
    
# Representa un gráfico de barras de comparación entre una variable cualitativa y la variable objetivo Precio
def bivar_graph_categ(_df, _var, _obj_var="Precio"):
    plt.figure(figsize=(12,6))
    chart = sns.barplot(data=_df, x=_var, y=_obj_var, color='royalblue')
    if _df[_var].nunique() > 20:
        chart.set_xticklabels([])
    else:
        chart.set_xticklabels(chart.get_xticklabels(), rotation=45, horizontalalignment='right')
    
    plt.show()
    
    
# Representa un gráfico boxplot de comparación entre una variable cualitativa y la variable objetivo Precio
def bivar_graph_boxplot(_df, _var, _obj_var="Precio"):
    plt.figure(figsize=(12, 6))
    sns.boxplot(x= _var, y=_obj_var, data=_df)
    plt.xticks(rotation=65)
    plt.title(f'Boxplot de {_obj_var} por {_var}')
    
    plt.show()
    
    
# Representa un scatterplot de comparación entre una variable cuantitativa y la variable objetivo Precio 
def bivar_graph_scatt(_df, _var, _obj_var="Precio"):
    plt.figure(figsize=(10, 6))
    sns.regplot(x=_var, y=_obj_var, data=_df, scatter_kws={"s": 20}, line_kws={"color": "red"})
    plt.title(f"Comparación de {_var} y {_obj_var}")
    plt.xlabel(_var)
    plt.ylabel(_obj_var)
    
    plt.show()

    
# Representa la matriz de correlación de las variables numéricas del dataframe
def multivar_graph_correlation_matrix(_df):
    corr = _df.corr()
    mask = np.triu(np.ones_like(corr, dtype=bool))
    
    f, ax = plt.subplots(figsize=(11, 9))
    cmap = sns.diverging_palette(230, 20, as_cmap=True)
    sns.set_theme(style="white")
    sns.heatmap(corr, mask=mask, cmap=cmap, vmax=1, vmin=-1, center=0,
                square=True, linewidths=.5, cbar_kws={"shrink": .5});
    

# Representa un gráfico comparando cada predicción con su residuo
def residuals_vs_predictions(_pred, _resid):
    plt.figure(figsize=(14, 6))

    plt.scatter(_pred, _resid, color='orange', alpha=0.6)
    plt.axhline(0, color='black', linestyle='--')
    plt.title("Residuos vs Predicciones")
    plt.xlabel("Predicciones")
    plt.ylabel("Residuos")

    
# Representa un gráfico comparando los valores de test con los valores predichos por el modelo
def test_vs_estimacion(_y_test, _y_pred):
    fig, ax = plt.subplots(figsize=(12,8))
    plt.scatter(_y_test, _y_pred,  color='royalblue')
    plt.plot(_y_test, _y_test, color='black', linewidth=3)

    plt.xlabel("Test")
    plt.ylabel("Estimación")
    plt.show()
    

# Representa un histograma de distribución de residuos
def residual_histogram(_residuals, _bins=20):
    plt.figure(figsize=(14, 6))

    plt.hist(_residuals, bins=_bins, color='orange', alpha=1)
    plt.title("Histograma de Residuos")
    plt.xlabel("Residuos")

    plt.tight_layout()
    plt.show()
    
    
# Representa un gráfico QQPlot para comparar la normalidad de los residuos
def residuals_qqplot(_residuals):
    plt.figure(figsize=(14, 6))

    stats.probplot(_residuals, dist="norm", plot=plt)
    plt.title("QQPlot de Residuos")

    plt.tight_layout()
    plt.show()
In [5]:
## MODELOS ##
# Realiza un LOF y devuelve el número de outliers y el índice de todos los registros que contienen al menos un outlier
def local_outlier_factor(_df, _x, _y, _n_neighbors=20):
    clf = LocalOutlierFactor(n_neighbors=_n_neighbors, contamination="auto")
    y_pred = clf.fit_predict(_x)
    outliers_indices = [i for i, val in enumerate(y_pred) if val == -1]
    outliers = _df[y_pred == -1]
    n_outliers = sum(y_pred==-1)
    n_total = len(y_pred)
    x_scores = clf.negative_outlier_factor_
    radius = (x_scores.max() - x_scores) / (x_scores.max() - x_scores.min())
    print(f"El número de outliers es de {n_outliers} de {n_total}")
    
    return n_outliers, outliers_indices


# Grafica un estudio de la varianza explicada acumulada para saber el número de componentes a emplear en PCA
def pca_n_components(_x):
    pca = PCA()
    pca.fit(_x)

    explained_variance = pca.explained_variance_ratio_
    cumulative_variance = np.cumsum(explained_variance)

    n_components_90 = np.argmax(cumulative_variance >= 0.9) + 1

    plt.plot(range(1, len(_x.columns) + 1), explained_variance, color="orange", label="Varianza Explicada")
    plt.plot(range(1, len(_x.columns) + 1), cumulative_variance, label="Varianza Acumulada")

    plt.axhline(0.9, color="black", linestyle="--")
    plt.axvline(n_components_90, color="red", linestyle="--")

    # Añadir anotación en el gráfico
    plt.annotate(f'90% en {n_components_90} componentes',
                 xy=(n_components_90, 0.9),
                 xytext=(n_components_90 + 5, 0.8),
                 arrowprops=dict(facecolor='black', shrink=0.05))

    plt.xlabel("Número de componentes principales")
    plt.ylabel("Proporción de varianza explicada")
    plt.legend()

    plt.show()
    
    
# Devuelve un dataframe con los n componentes tras aplicar PCA
def pca_dataframe(_x, _n_components):
    pca = PCA(n_components=_n_components)
    pca.fit(_x)
    comp_principales = pca.transform(_x)

    return pd.DataFrame(data=comp_principales, columns=[f'PC{i+1}' for i in range(_n_components)])


# Entrenamiento de un modelo de regresión de Ridge
def ridge_regression_fit(_x_train, _x_test,  _y_train, _y_test, _alpha=1.0):
    ridge_model = Ridge(alpha=_alpha)

    ridge_model.fit(_x_train, _y_train)

    y_pred = ridge_model.predict(_x_test)

    _mse = round(mean_squared_error(_y_test, y_pred),2)
    _rmse = round(np.sqrt(_mse),2)
    _r2_score = round(r2_score(_y_test, y_pred),2)
    print(f"RMSE de Ridge Regression: {_rmse}\nR2 score de Ridge Regression: {_r2_score}")
    
    return ridge_model, {"MSE": _mse, "RMSE": _rmse, "R2": _r2_score}


# Entrenamiento de un modelo Random Forest para regresión
def random_forest_regression_fit(_x_train, _x_test,  _y_train, _y_test):
    rf_model = RandomForestRegressor()

    rf_model.fit(_x_train, _y_train)

    y_pred = rf_model.predict(_x_test)

    _mse = round(mean_squared_error(_y_test, y_pred),2)
    _rmse = round(np.sqrt(_mse),2)
    _r2_score = round(r2_score(_y_test, y_pred),2)
    print(f"RMSE de Random Forest: {_rmse}\nR2 score de Random Forest: {_r2_score}")
    
    return rf_model, {"MSE": _mse, "RMSE": _rmse, "R2": _r2_score}


# Entrenamiento de un modelo XGBoost
def xgboost_fit(_x_train, _x_test,  _y_train, _y_test):
    xgb_model = XGBRegressor()

    xgb_model.fit(_x_train, _y_train)

    y_pred = xgb_model.predict(_x_test)

    _mse = round(mean_squared_error(_y_test, y_pred),2)
    _rmse = round(np.sqrt(_mse),2)
    _r2_score = round(r2_score(_y_test, y_pred),2)
    print(f"RMSE de XGBoost: {_rmse}\nR2 score de XGBoost: {_r2_score}")
    
    return xgb_model, {"MSE": _mse, "RMSE": _rmse, "R2": _r2_score}

Resolución¶

En primer lugar, la compañía nos pide que realicemos un proyecto completo de analítica avanzada siguiendo la metodología CRISP-DM. Esta metodología consta de las siguientes fases:

  1. Entendimiento del negocio: qué y para qué se pretende implementar el modelo predictivo.
  2. Entendimiento de los datos: recolección y análisis exploratorio de los datos
  3. Preparación de los datos: preprocesamiento, transformación y selección de variables y registros.
  4. Modelado: selección e implementación del modelo predictivo.
  5. Evaluación del modelo: contraste de resultados y recalibrado del mismo.
  6. Despliegue del modelo: realizar la presentación visual del proyecto.

1. Entendimiento del negocio:¶

La compañía, dedicada a la prestación de servicios de telecomunicaciones, pretende diversificar sus actividades y ha detectado un posible nicho de mercado en la venta de coches de segunda mano a sus clientes habituales (residentes no nacionales), por lo que necesita conocer la estimación de precios de los coches de segunda mano para poder realizar ofertas competitivas a sus clientes.

Ésto se traduce en la estimación de una variable Precio, para lo cuál se estudiarán los datos que se poseen y se tratará de elaborar un modelo predictivo que logre dicha estimación en función de ciertas características de los coches.

2. Entendimiento de los datos:¶

2.1. Orígenes de los datos:¶

Los datos, en lo que respecta al departamento de Ciencia de Datos, son de origen interno, ya que, en el momento en que el proyecto es encargado, la empresa ya cuenta con un archivo .csv con todos los datos. Se desconoce el momento, la forma y el origen inicial de dichos datos, si fueron recogidos por la propia empresa (creados) o adquiridos de un tercero externo.

Se trata de datos estructurados, recogidos en forma de tabla. Se desconoce el número de columnas (variables) y observaciones (registros), aunque se conocerá tan pronto se cargue el .csv en el proyecto. Se da por hecho que debe existir una variable Precio, ya que es necesaria para el ajuste del modelo.

2.2. Colección inicial de datos:¶

Se procede a la carga del fichero de datos para comenzar su análisis exploratorio.

In [6]:
df_original = pd.read_csv(r"archivos/Coches_Segunda_Mano.csv")
df_original
Out[6]:
Marca Modelo Año Combustible CV Cilindros Transmisión Tracción Puertas Mercado Tamaño Estilo Consumo Carretera Consumo Ciudad Popularidad Precio
0 BMW 1 Series M 2011 premium unleaded (required) 335.0 6.0 MANUAL rear wheel drive 2.0 Factory Tuner,Luxury,High-Performance Compact Coupe 26.0 19.0 3916 46135.0
1 BMW 1 Series 2011 premium unleaded (required) 300.0 6.0 MANUAL rear wheel drive 2.0 Luxury,Performance Compact Convertible 28.0 19.0 3916 40650.0
2 BMW 1 Series 2011 premium unleaded (required) 300.0 6.0 MANUAL rear wheel drive 2.0 Luxury,High-Performance Compact Coupe 28.0 20.0 3916 36350.0
3 BMW 1 Series 2011 premium unleaded (required) 230.0 6.0 MANUAL rear wheel drive 2.0 Luxury,Performance Compact Coupe 28.0 18.0 3916 29450.0
4 BMW 1 Series 2011 premium unleaded (required) 230.0 6.0 MANUAL rear wheel drive 2.0 Luxury Compact Convertible 28.0 18.0 3916 34500.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
11909 Acura ZDX 2012 premium unleaded (required) 300.0 6.0 AUTOMATIC all wheel drive 4.0 Crossover,Hatchback,Luxury Midsize 4dr Hatchback 23.0 16.0 204 46120.0
11910 Acura ZDX 2012 premium unleaded (required) 300.0 6.0 AUTOMATIC all wheel drive 4.0 Crossover,Hatchback,Luxury Midsize 4dr Hatchback 23.0 16.0 204 56670.0
11911 Acura ZDX 2012 premium unleaded (required) 300.0 6.0 AUTOMATIC all wheel drive 4.0 Crossover,Hatchback,Luxury Midsize 4dr Hatchback 23.0 16.0 204 50620.0
11912 Acura ZDX 2013 premium unleaded (recommended) 300.0 6.0 AUTOMATIC all wheel drive 4.0 Crossover,Hatchback,Luxury Midsize 4dr Hatchback 23.0 16.0 204 50920.0
11913 Lincoln Zephyr 2006 regular unleaded 221.0 6.0 AUTOMATIC front wheel drive 4.0 Luxury Midsize Sedan 26.0 17.0 61 28995.0

11914 rows × 16 columns

In [7]:
df_original.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 11914 entries, 0 to 11913
Data columns (total 16 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   Marca              11914 non-null  object 
 1   Modelo             11914 non-null  object 
 2   Año                11914 non-null  int64  
 3   Combustible        11911 non-null  object 
 4   CV                 11845 non-null  float64
 5   Cilindros          11884 non-null  float64
 6   Transmisión        11914 non-null  object 
 7   Tracción           11914 non-null  object 
 8   Puertas            11908 non-null  float64
 9   Mercado            8172 non-null   object 
 10  Tamaño             11914 non-null  object 
 11  Estilo             11914 non-null  object 
 12  Consumo Carretera  11914 non-null  float64
 13  Consumo Ciudad     11914 non-null  float64
 14  Popularidad        11914 non-null  int64  
 15  Precio             11914 non-null  float64
dtypes: float64(6), int64(2), object(8)
memory usage: 1.5+ MB

Se cuenta con una colección inicial de datos formada por 16 variables y 11914 observaciones.

2.3. Análisis Exploratorio:¶

2.3.1. Descripción de los datos:

Como se ha adelantado en el punto anterior, la composición inicial del set de datos es de 16 columnas y 11914 observaciones. En principio, no se va a reducir la cantidad de registros mediante muestreo ni la dimensionalidad (aunque es posible que, a lo largo del análisis se tome la decisión contraria y se proceda a reducir la dimensionalidad en el punto 3º de la metodología CRISP-DM: preparación de los datos).

Las variables Año, CV, Cilindros, Puertas, Consumo Carretera, Consumo Ciudad, Popularidad y Precio son variables numéricas (cuantitativas). Se confirma la existencia de la variable objetivo en el set de datos.

El resto de variables (object) son cualitativas; de hecho, son todas categóricas. En el caso específico de Mercado, parece que cada registro puede pertenecer a una o varias categorías, por lo que es posible que, llegado el momento, haya que dummificarla. También parece que existen muchos valores nulos para esta variable, por lo que habrá que estudiar si se eliminan dichos registros o se imputa un valor (existente o creado, por ejemplo, "Otros").

In [8]:
# Copiamos el dataframe original para, en el caso de hacer cambios, hacerlos sobre una copia.
df = df_original.copy()
In [9]:
# Listas para tener controladas variables cualitativas y cuantitativas:
var_list = df.columns
var_list_categ = []
var_list_num = []

for var in var_list:
    if df[var].dtype == "object":
        var_list_categ.append(var)
    elif df[var].dtype == "int64" or df[var].dtype == "float64":
        var_list_num.append(var)
    else:
        pass
In [10]:
var_list_categ
Out[10]:
['Marca',
 'Modelo',
 'Combustible',
 'Transmisión',
 'Tracción',
 'Mercado',
 'Tamaño',
 'Estilo']
In [11]:
var_list_num
Out[11]:
['Año',
 'CV',
 'Cilindros',
 'Puertas',
 'Consumo Carretera',
 'Consumo Ciudad',
 'Popularidad',
 'Precio']

En principio, se consideran Año, Cilindros y Puertas variables discretas, y el resto continuas. Para estas 3 variables, realizar una media no tiene mucho sentido, ya que ningún coche puede tener 3.5 puertas o 6.75 cilindros, por ejemplo, pero aún así, sus categorías son valores numéricos que pueden ser ordenados, ya que 2 puertas son menos que 4, 8 cilindros menos que 16, y el año 1990 es anterior al año 2000. Podrían considerarse que están a nivel ordinal.

In [12]:
var_list_num_disc = ["Año", "Cilindros", "Puertas"]
var_list_num_cont = [var for var in var_list_num if var not in var_list_num_disc]

2.3.2. Análisis univariante:

Con este análisis se espera conocer mejor la distribución de las variables. En el caso de las cuantitativas, también se espera poder identificar mejor cuáles serán consideradas continuas y cuáles discretas.

In [13]:
# Número de categorías únicas y moda para cada variable cualitativa:
df_univar_categ = univar_categ(df, var_list_categ)
df_univar_categ
Out[13]:
Variable Valores Únicos Moda
0 Marca 48 Chevrolet
1 Modelo 915 Silverado 1500
2 Combustible 10 regular unleaded
3 Transmisión 5 AUTOMATIC
4 Tracción 4 front wheel drive
5 Mercado 71 NaN
6 Tamaño 3 Compact
7 Estilo 16 Sedan

Se destaca el hecho de que el valor más común en la variable Mercado es NaN o nulo, lo que remarca la necesidad de solventar este problema cuando llegue el momento.

In [14]:
for var in var_list_categ:
    univar_graph_categ(df, var)

Para aquellas variables con un número de categorías o valores únicos mayor de 20 se representarán las 20 categorías con mayor frecuencia y se representará a continuación su distribución completa junto al porcentaje acumulado, eliminando las etiquetas, para poder visualizar si existen categorías mucho más frecuentes que otras o si la distribución de ellas es más o menor pareja.

En el caso de Marca, vemos que existen 48 valores únicos, siendo representadas en el gráfico las 20 marcas más frecuentes de nuestro dataset. Cabe destacar que, de las 48 marcas, tan sólo 10 superan las 400 unidades vendidas, con una fuerte caída desde la marca más vendida (única por encima de 1000 unidades) hasta estabilizarse y continuar con un descenso de las frecuencias mucho más pausado.

En los Modelos pasa algo similar, siendo 915 valores únicos a representar se decide graficar tan sólo los 20 modelos más vendidos (aunque a continuación se presentará la distribución de todos sus valores). Y, al igual que con la Marca, los primeros modelos acumulan frecuencias bastante más elevadas, lo que nos deja un descenso brusco en el inicio hasta continuar con un descenso mucho más suave.

El Combustible está bastante dominado por una única categoría, regular unleaded, pues acumula más del 50% de las observaciones (más de 7000).

En el caso de la Transmisión pasa lo mismo: AUTOMATIC concentra más de 8000 observaciones, lo que implica que es bastante más del 50% de los datos.

El gráfico de Mercados es poco representativo, ya que, por un lado, existen muchos valores nulos en esta variable (de hecho, como se ha visto, son la categoría más frecuente), y, por otro lado, existen combinaciones de categorías. Como se ha adelantado, es muy probable que haya que dummificarla cuando llegue el momento de realizar transformaciones.

Y finalmente, de la distribución del Estilo se puede comentar lo mismo que en el caso de la Marca y el Modelo, exceptuando el hecho de que, en este caso, están representadas todas las categorías, dado que no superan los 20 valores distintos.

A continuación, se representarán las distribuciones completas de aquellas variables que han tenido que representar tan sólo los valores más frecuentes, y se pondrán en relación con el porcentaje acumulado de forma que se puedan buscar patrones, como grandes acumulaciones seguidas de una larga cola, por ejemplo.

In [15]:
for var in ["Marca", "Modelo", "Mercado"]:
    univar_graph_categ_extra(df, var)

En los 3 casos (Marca, Modelo y Mercado) se puede observar una gran acumulación de frecuencias en las categorías con mayores frecuencias (algo que prácticamente ya se veía representando tan sólo las 20 primeras categorías), destacando que es en el Mercado donde existe una mayor acumulación en los valores más frecuentes, como se puede observar en la curva de porcentaje acumulado más inclinada que las otras dos.

In [16]:
df_univar_num_cont = univar_num_cont(df, var_list_num_cont)
df_univar_num_cont
Out[16]:
Variable Valores Únicos Media Mediana Moda Mínimo Máximo
0 CV 356 249.39 227.0 200.0 55.0 1001.0
1 Consumo Carretera 59 26.64 26.0 24.0 12.0 354.0
2 Consumo Ciudad 69 19.73 18.0 17.0 7.0 137.0
3 Popularidad 48 1554.91 1385.0 1385.0 2.0 5657.0
4 Precio 6049 40594.74 29995.0 2000.0 2000.0 2065902.0
In [17]:
df_univar_num_disc = univar_num_disc(df, var_list_num_disc)
df_univar_num_disc
Out[17]:
Variable Valores Únicos Mediana Moda Mínimo Máximo
0 Año 28 2015.0 2015.0 1990.0 2017.0
1 Cilindros 9 6.0 4.0 0.0 16.0
2 Puertas 3 4.0 4.0 2.0 4.0

Se considerarán variables discretas Año, Cilindros y Puertas.

Se considerarán variables continuas CV, Consumo Carretera, Consumo Ciudad, Popularidad y Precio. Es cierto que ninguna de ellas puede tomar valores infinitos, dado que todas parecen estar redondeadas a la unidad, pero se considera que la diferencia entre el mínimo y el máximo es suficiente como para que puedan tomar muchos valores, haciendo muy costoso el tratamiento como variables discretas. Una variable discreta con muchos valores distintos puede dificultar la visualización de gráficas durante el EDA y, además, puede llevar a causar overfitting en modelos predictivos si éstos tratan de capturar patrones específicos de estas variables (a excepción de ciertos modelos como, por ejemplo, los algoritmos basados en árboles de decisión, que son robustos ante muchas categorías).

In [18]:
for var in var_list_num_cont:
    univar_graph_num_cont(df, var)

Se puede observar que en las distribuciones de CV, Consumo Carretera, Consumo Ciudad y Precio existe un fuerte sesgo a la derecha (asimetría positiva), acumulando la mayor parte de los valores a la izquierda de los gráficos. Esto es indicador de que posiblemente existan valores outliers por lo que, llegado el momento, se realizará un estudio de éstos y se tomará una decisión con respecto a cómo actuar frente a ellos (si mantenerlos, manipularlos o eliminarlos).

Por otro lado, Popularidad tiene una distribución muy diferente, sin un sesgo obvio, aunque con una gran acumulación a la izquierda, una serie de valores vacíos o con muy poca frecuencia y, finalmente, una barra apartada y solitaria a la derecha. Sería interesante conocer el significado real de "Popularidad", pues, si se tratara de una valoración personal, se podría interpretar que la mayoría de coches no goza de mucha popularidad mientras, por el contrario, un grupo de ellos tiene una valoración muy superior al resto.

In [19]:
for var in var_list_num_disc:
    univar_graph_num_disc(df, var)

En el caso de la distribución de Año, cabe destacar que los coches modernos son mucho más numerosos que los antiguos, siendo los coches de 2015, 2016 y 2017 los que acumulan gran parte de los valores de la distribución.

En cuanto a los Cilindros, la mayoría de coches tiene 4 o 6 cilindros, aunque también hay un buen número de coches con 8 cilindros. Mucho menos numerosos son los de 3, 10, 12 y 16 cilindros. Se considera que los coches con 0 cilindros son coches eléctricos, los cuáles no los necesitan.

Finalmente, la mayoría de coches tiene 4 Puertas.

2.3.3. Análisis bivariante:

Con este análisis se espera obtener una idea de qué variables están relacionadas de manera lineal o no lineal con la variable objetivo para así seleccionar qué conjunto de variables predictoras serán las empleadas en el modelo, y también se hará una primera selección de los posibles modelos que podrían ser empleados.

Respecto de las variables cualitativas, se representarán gráficamente en relación con el precio y se presentará un boxplot para ver con claridad si existen valores outliers.

In [20]:
for var in [var for var in var_list_categ if var != "Modelo"]:
    bivar_graph_categ(df, var)
In [21]:
for var in [var for var in var_list_categ if var != "Modelo"]:
    bivar_graph_boxplot(df, var)

Teniendo en consideración los gráficos anteriores, se puede constatar lo siguiente:

  • La Marca parece tener una influencia no muy grande en el precio, a excepción de algunos casos particulares, como marcas consideradas de lujo (Ferrari, McLaren, Maybach, Porsche, Bentley, etc.). Bugatti parece estar muy por encima del resto en cuanto a precio.
  • El Mercado también parece que influencia al precio, pues existe mucha variabilidad en el precio en función del tipo de mercado al que está dirigido.
  • El resto de variables parecen influenciar al precio de manera más clara, con categorías que claramente obtienen mayores valores de precio que otras.
  • Existe un gran número de outliers en las representaciones gráficas de todas las variables, por lo que será necesario tratarlas llegado el momento.

Por otro lado, la variable "Modelo" no se incluye en este análisis debido a que su alto número de valores únicos. Aunque es una variable que puede tener un impacto en el precio del coche, 915 valores únicos hacen imposible una representación gráfica clara; además, se considera que el modelo podría estar capturando en parte la marca, el estilo y el tamaño, entre otras variables.

Con respecto a las variables cuantitativas continuas, se representarán en scatterplots su relación con la variable objetivo, y se calculará para cada una de ellas el coeficiente de correlación de Pearson.

In [22]:
correlations = []

for var in [var for var in var_list_num_cont if var != "Precio"]:
    bivar_graph_scatt(df, var)
    correlation, p_value = pearsonr(df[var], df["Precio"])
    correlations.append({"Variable":var, "R":correlation, "p-valor":p_value})
In [23]:
for var in [var for var in var_list_num if var != "Precio"]:
    bivar_graph_boxplot(df, var)
In [24]:
correlations
Out[24]:
[{'Variable': 'CV', 'R': nan, 'p-valor': nan},
 {'Variable': 'Consumo Carretera',
  'R': -0.16004267885202625,
  'p-valor': 3.4775933606016465e-69},
 {'Variable': 'Consumo Ciudad',
  'R': -0.15767572242668287,
  'p-valor': 3.4938535989394835e-67},
 {'Variable': 'Popularidad',
  'R': -0.04847623245504457,
  'p-valor': 1.1980083973487247e-07}]

A la vista de los resultados obtenidos, se puede concluir que:

  • No se ha podido obtener el cálculo de R para la variable CV respecto del Precio, posiblemente debido a la existencia de valores nulos en esta variable. Visualmente, parece existir cierta relación lineal positiva, a tenor de la línea de correlación de la gráfica.
  • El resto de variables tienen relaciones lineales negativas con el Precio, si bien muy débiles; en el caso de Popularidad, casi inexistente.
  • En cualquier caso, para las 3 variables, los p-valores muy inferiores a 0.05 indican que son estadísticamente significativas.

Se procederá a crear un nuevo dataframe temporal eliminando los valores nulos de forma que se pueda calcular el coeficiente R de Pearson para le resto de valores de CV y el Precio

In [25]:
df_temp = df.copy().dropna(subset=["CV"])
df_temp.count()
Out[25]:
Marca                11845
Modelo               11845
Año                  11845
Combustible          11842
CV                   11845
Cilindros            11816
Transmisión          11845
Tracción             11845
Puertas              11844
Mercado               8114
Tamaño               11845
Estilo               11845
Consumo Carretera    11845
Consumo Ciudad       11845
Popularidad          11845
Precio               11845
dtype: int64

A continuación, se vuelve a calcular el coeficiente R de Pearson para CV y Precio:

In [26]:
correlation_cv, p_value_cv = pearsonr(df["CV"], df["Precio"])
print(f"R: {correlation_cv}; p-valor: {p_value_cv}")
R: nan; p-valor: nan

Los resultados continúan siendo los mismos, por lo que el problema no se encuentra en el hecho de que existan valores nulos en la variable CV.

Por tanto, no parece que vayan a ser variables que expliquen una gran parte de la variabilidad del precio, al menos en una relación lineal.

In [27]:
correlations_log = []

for var in [var for var in var_list_num_cont if var != "Precio"]:
    correlation, p_value = pearsonr(np.log(df[var]), df["Precio"])
    correlations_log.append({"Variable":var, "R":correlation, "p-valor":p_value})

correlations_log
Out[27]:
[{'Variable': 'CV', 'R': nan, 'p-valor': nan},
 {'Variable': 'Consumo Carretera',
  'R': -0.23183085649970492,
  'p-valor': 3.9939167862142234e-145},
 {'Variable': 'Consumo Ciudad',
  'R': -0.29124355505787913,
  'p-valor': 1.3310344014511368e-231},
 {'Variable': 'Popularidad',
  'R': -0.07489823527192474,
  'p-valor': 2.703234403513693e-16}]
In [28]:
correlations_inv = []

for var in [var for var in var_list_num_cont if var != "Precio"]:
    correlation, p_value = pearsonr(1/df[var], df["Precio"])
    correlations_inv.append({"Variable":var, "R":correlation, "p-valor":p_value})

correlations_inv
Out[28]:
[{'Variable': 'CV', 'R': nan, 'p-valor': nan},
 {'Variable': 'Consumo Carretera',
  'R': 0.2664601177054636,
  'p-valor': 8.64280527164103e-193},
 {'Variable': 'Consumo Ciudad', 'R': 0.38289741107464936, 'p-valor': 0.0},
 {'Variable': 'Popularidad',
  'R': 0.05156111999020441,
  'p-valor': 1.790463470843801e-08}]
In [29]:
correlations_squ = []

for var in [var for var in var_list_num_cont if var != "Precio"]:
    correlation, p_value = pearsonr(np.power(df[var], 2), df["Precio"])
    correlations_squ.append({"Variable":var, "R":correlation, "p-valor":p_value})

correlations_squ
Out[29]:
[{'Variable': 'CV', 'R': nan, 'p-valor': nan},
 {'Variable': 'Consumo Carretera',
  'R': -0.048180638553286526,
  'p-valor': 1.4289910530638145e-07},
 {'Variable': 'Consumo Ciudad',
  'R': -0.049667017107271574,
  'p-valor': 5.8280968556232164e-08},
 {'Variable': 'Popularidad',
  'R': -0.04108762187503665,
  'p-valor': 7.250810957399716e-06}]
In [30]:
correlations_squ2 = []

for var in [var for var in var_list_num_cont if var != "Precio"]:
    correlation, p_value = pearsonr(np.power(df[var], 1/2), df["Precio"])
    correlations_squ2.append({"Variable":var, "R":correlation, "p-valor":p_value})

correlations_squ2
Out[30]:
[{'Variable': 'CV', 'R': nan, 'p-valor': nan},
 {'Variable': 'Consumo Carretera',
  'R': -0.20280792842328668,
  'p-valor': 8.205753429414463e-111},
 {'Variable': 'Consumo Ciudad',
  'R': -0.22892567292076202,
  'p-valor': 1.8339546341215385e-141},
 {'Variable': 'Popularidad',
  'R': -0.06124750214553778,
  'p-valor': 2.2191903228257136e-11}]

Aplicadas diversas transformaciones, los resultados siguen siendo los mismos: la relación lineal entre las variables Consumo Carretera, Consumo Ciudad y Popularidad y sus transformaciones con la variable Precio son muy débiles y, en algunos casos, casi nula.

In [31]:
for var in var_list_num_disc:
    bivar_graph_categ(df, var)
    correlation, p_value = pearsonr(df[var], df["Precio"])
    correlations.append({"Variable":var, "R":correlation, "p-valor":p_value})

Para las variables discretas se empleará el mismo tipo de gráficas que para las variables categóricas. Para el caso de la variable Año, aunque podría tratarse como una secuencia temporal y graficarse con un gráfico de línea, no se considera importante la comparación año a año (como sí podría serlo en un estudio de venta de coches, en el que la evolución temporal de la venta de coches por años sí es interesante de estudiar), sino que el año es tan sólo el dato necesario para conocer lo viejo o nuevo que es un coche de seguna mano. Por ello, dado que no es interesante este tipo de comparaciones año a año, se decide realizar un gráfico de barras.

In [32]:
correlations[4:]
Out[32]:
[{'Variable': 'Año',
  'R': 0.22758951410473433,
  'p-valor': 8.520188245566554e-140},
 {'Variable': 'Cilindros', 'R': nan, 'p-valor': nan},
 {'Variable': 'Puertas', 'R': nan, 'p-valor': nan}]
In [33]:
df_temp = df.copy().dropna(subset=["Puertas", "Cilindros"])
df_temp.info()
<class 'pandas.core.frame.DataFrame'>
Index: 11878 entries, 0 to 11913
Data columns (total 16 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   Marca              11878 non-null  object 
 1   Modelo             11878 non-null  object 
 2   Año                11878 non-null  int64  
 3   Combustible        11875 non-null  object 
 4   CV                 11815 non-null  float64
 5   Cilindros          11878 non-null  float64
 6   Transmisión        11878 non-null  object 
 7   Tracción           11878 non-null  object 
 8   Puertas            11878 non-null  float64
 9   Mercado            8136 non-null   object 
 10  Tamaño             11878 non-null  object 
 11  Estilo             11878 non-null  object 
 12  Consumo Carretera  11878 non-null  float64
 13  Consumo Ciudad     11878 non-null  float64
 14  Popularidad        11878 non-null  int64  
 15  Precio             11878 non-null  float64
dtypes: float64(6), int64(2), object(8)
memory usage: 1.5+ MB
In [34]:
correlation_puertas, p_value_puertas = pearsonr(df["Puertas"], df["Precio"])
print(f"R: {correlation_puertas}; p-valor: {p_value_puertas}")
R: nan; p-valor: nan
In [35]:
correlation_cilindros, p_value_cilindros = pearsonr(df["Cilindros"], df["Precio"])
print(f"R: {correlation_cilindros}; p-valor: {p_value_cilindros}")
R: nan; p-valor: nan

Al igual que en el caso de CV, eliminando los valores nulos de Puertas y Cilindros no desaparece el problema.

En cualquier caso, atendiendo a las gráficas, Puertas y Año no parecen tener una fuerte relación lineal (de hecho, Precio y Año tienen un R de 0.22, lo que indica una relación lineal positiva muy débil - aunque con un p-valor muy inferior a 0.05, lo que indica alta significación estadística).

2.3.4. Análisis multivariante:

Con este análisis se espera conocer las relaciones lineales entre las distintas variables cuantitativas de manera que se puedan eliminar alguna de entre los pares de variables con mayor correlación. Se empleará para ello el límite de 0.9 en el coeficiente de correlación.

In [36]:
multivar_graph_correlation_matrix(df[var_list_num])
In [37]:
correlation_consumos = pearsonr(df["Consumo Ciudad"], df["Consumo Carretera"])
correlation_consumos
Out[37]:
PearsonRResult(statistic=0.8868294962591363, pvalue=0.0)
In [38]:
correlation_cilindros_cv = pearsonr(df["Cilindros"], df["CV"])
correlation_cilindros_cv
Out[38]:
PearsonRResult(statistic=nan, pvalue=nan)

De la matriz anterior se concluye que no existen grandes correlaciones entre las variables del dataset, a excepción de Cilindros y CV (aunque es imposible cuantificarlo dado que le coeficiente de correlación arroja un resultado nulo), y entre las dos variables de consumo (cuyo coeficiente de correlación R es de 0.8868, inferior por poco a 0.9, por lo que se decide no eliminar ninguna de ellas).

Por otro lado, parece que sólo las variables CV y Cilindros están linealmente relacionadas con el Precio. Año parece tener una relación lineal positiva débil, Puertas y Popularidad parecen no tener apenas relación lineal, y Consumo Carretera y Consumo Ciudad tienen una relación lineal negativa débil con Precio.

In [39]:
df[var_list_num].corr()
Out[39]:
Año CV Cilindros Puertas Consumo Carretera Consumo Ciudad Popularidad Precio
Año 1.000000 0.351794 -0.041479 0.263787 0.258240 0.198171 0.073049 0.227590
CV 0.351794 1.000000 0.779988 -0.102713 -0.406563 -0.439371 0.037501 0.662008
Cilindros -0.041479 0.779988 1.000000 -0.140088 -0.621606 -0.600776 0.041145 0.531312
Puertas 0.263787 -0.102713 -0.140088 1.000000 0.118570 0.120881 -0.048272 -0.126635
Consumo Carretera 0.258240 -0.406563 -0.621606 0.118570 1.000000 0.886829 -0.020991 -0.160043
Consumo Ciudad 0.198171 -0.439371 -0.600776 0.120881 0.886829 1.000000 -0.003217 -0.157676
Popularidad 0.073049 0.037501 0.041145 -0.048272 -0.020991 -0.003217 1.000000 -0.048476
Precio 0.227590 0.662008 0.531312 -0.126635 -0.160043 -0.157676 -0.048476 1.000000

Finalmente, y para ver con mayor precisión estas correlaciones, observamos la matriz de coeficientes de correlación, donde se confirma lo expuesto anteriormente con la visión de la matriz de correlaciones gráfica. Podemos también obtener algunas métricas de correlación que anteriormente resultaron nulas, como la correlación entre CV y Cilindros, que resulta ser 0.78 (inferior a 0.9, por lo que no es suficiente para eliminar una de ellas), o las correlaciones entre Cilindros y Puertas con Precio, que son 0.5313 y -0.1266 respectivamente.

Debido a la falta de relaciones lineales fuertes entre las variables numéricas y el Precio, la existencia de variables ordinales, y al hecho de que existen outliers en prácticamente la totalidad de éstas, se procede a estudiar los coeficientes de correlación de Spearman, los cuales nos darán una idea más aproximada de la existencia o no de otro tipos de relaciones no necesariamente lineales entre las variables cuantitativas y el Precio, sino basadas en rangos.

In [40]:
df[var_list_num].corr(method="spearman")
Out[40]:
Año CV Cilindros Puertas Consumo Carretera Consumo Ciudad Popularidad Precio
Año 1.000000 0.330883 -0.089457 0.241693 0.332119 0.301150 0.185043 0.517037
CV 0.330883 1.000000 0.749650 -0.014737 -0.492900 -0.583996 0.030181 0.833080
Cilindros -0.089457 0.749650 1.000000 -0.105626 -0.765168 -0.837934 0.022236 0.472439
Puertas 0.241693 -0.014737 -0.105626 1.000000 0.137166 0.158429 -0.060008 0.073034
Consumo Carretera 0.332119 -0.492900 -0.765168 0.137166 1.000000 0.951307 0.012727 -0.201473
Consumo Ciudad 0.301150 -0.583996 -0.837934 0.158429 0.951307 1.000000 0.021129 -0.273546
Popularidad 0.185043 0.030181 0.022236 -0.060008 0.012727 0.021129 1.000000 0.002283
Precio 0.517037 0.833080 0.472439 0.073034 -0.201473 -0.273546 0.002283 1.000000

De esta nueva matriz podemos destacar lo siguiente:

  • Año: correlación positiva moderada con el Precio.
  • CV: fuerte correlación positiva con el Precio.
  • Cilindros: correlación positiva moderada con el Precio, fuerte positiva con CV, y fuerte negativa con ambos Consumos.
  • Puertas: correlación casi inexistente con el Precio (se confirma con el coeficiente R=-0.1266).
  • Consumo Carretera y Consumo Ciudad: débil correlación negativa con el Precio, y extremadamente fuerte entre ellas.
  • Popularidad: correlación prácticamente inexistente con el Precio (se confirma con el coeficiente R=-0.0485).

Por tanto, las variables Puertas y Popularidad son candidatas a no ser incluidas en el entrenamiento del modelo predictivo.

Por otro lado, las variables de Consumo tienen una correlación muy cercana a la unidad, por lo que se valora el no incluir ambas en el modelo. Una opción podría ser el crear una variable que sustituya a ambas y suponga un Consumo Medio, como media de ambos Consumos.

2.4. Calidad de los datos:¶

2.4.1. Identificación de valores nulos o faltantes:

El primer paso en el estudio de la calidad de los datos será comprobar la existencia o no (ya hemos comprobado anteriormente que sí) de valores nulos o faltantes. Aunque ya se ha demostrado anteriormente que existen para varias variables (de hecho, para Mercado es la más común, la moda), se calculará ahora el ratio de densidad para conocer si faltan muchos o pocos valores con respecto al total de observaciones.

In [41]:
nulos_list = {}

for var in var_list:
    nulos_list[var] = round((df[var].isnull().sum()/max(df.count()))*100,2)
    
nulos_list
Out[41]:
{'Marca': 0.0,
 'Modelo': 0.0,
 'Año': 0.0,
 'Combustible': 0.03,
 'CV': 0.58,
 'Cilindros': 0.25,
 'Transmisión': 0.0,
 'Tracción': 0.0,
 'Puertas': 0.05,
 'Mercado': 31.41,
 'Tamaño': 0.0,
 'Estilo': 0.0,
 'Consumo Carretera': 0.0,
 'Consumo Ciudad': 0.0,
 'Popularidad': 0.0,
 'Precio': 0.0}

Viendo el ratio de densidad, aunque Combustible, CV, Cilindros y Puertas tienen valores nulos, éstos no son representativos pues en todos los casos repersenta menos del 1% del total de observaciones. En el caso de Mercado, como ya se había adelantado, suponen el 31.41% de los datos, por lo que se requiere de una solución.

Hubiera sido mucho más práctico realizar tanto esta identificación de valores nulos como su solución en una fase mucho más temprana, al empezar el EDA, pero dado que nos han solicitado que sigamos el esquema CRISP-DM, no se pondrá solución a ésto hasta llegar a la fase de preparación de los datos.

2.4.2. Identificación de registros duplicados:

En el caso que nos ocupa, dado que no existe una variable que convierta cada registro en una observación única, podrían no entenderse dos registros con los mismos datos como una observación repetida, por lo que no se eliminarían, ya que podría darse el caso de estar ante dos coches diferentes pero de la misma marca, modelo, año, precio, etc. Sin una clave identificadora (por ejemplo, podría ser la matrícula), dos registros no pueden ser considerados el mismo coche sólo por sus características idénticas. En cualquier caso, se trabajará bajo la hipótesis de que sí se refieren al mismo coche, por lo que se procederá a eliminar los registros duplicados.

In [42]:
print(f"Número de registros actual: {max(df.count())}")
df = df.drop_duplicates()
print(f"Número de registros tras eliminación de duplicados: {max(df.count())}")
Número de registros actual: 11914
Número de registros tras eliminación de duplicados: 11199

2.5. Identificación de outliers:¶

Ya pudimos comprobar durante el EDA que existen numerosas variables con valores outliers; de hecho, son tan numerosas y están presentes en tantas variables, que se descarta la eliminación sistemática de todos ellos y se confía su eliminación más precisa a un método de detección de anomalías. En cualquier caso, dado que el método seleccionado es el Local Outlier Factor, y éste precisa de la no existencia de valores nulos, se pospondrá su eliminación hasta haber eliminado o solucionado los valores nulos.

De nuevo, vemos cómo en la práctica es más óptimo identificar y solucionar el problema de los valores nulos tan pronto se empiece el análisis EDA. De momento, nos quedamos con haber identificado una gran cantidad de valores atípicos en la práctica totalidad de las variables.

3. Preparación de los datos:¶

En primer lugar, se seleccionarán los campos relevantes para el objetivo, por lo que se descartarán aquellos no predictivos, se limpiarán los datos restantes, se construirán nuevas variables a partir de las seleccionadas y se normalizarán, estandarizarán y discretizarán aquellas que lo necesiten. Finalmente, si el número de variables resultante es muy elevado, se empleará algún método para reducir la dimensionalidad antes de proceder a la modelización.

Eliminación de variables poco representativas:

Como se vio durante el análisis EDA, las variables Puertas y Popularidad no tienen apenas relación (ni lineal ni de otro tipo, atendiendo a los coeficientes de Pearson y Spearman), por lo que serán descartadas como variables explicativas.

In [43]:
df = df.drop(["Puertas", "Popularidad"], axis=1)
df.columns
Out[43]:
Index(['Marca', 'Modelo', 'Año', 'Combustible', 'CV', 'Cilindros',
       'Transmisión', 'Tracción', 'Mercado', 'Tamaño', 'Estilo',
       'Consumo Carretera', 'Consumo Ciudad', 'Precio'],
      dtype='object')

Otra variable susceptible de ser eliminada es Modelo. En primer lugar porque, al ser cualitativa y tener más de 900 valores distintos, supondrá generar más de 900 variables dummy llegado el momento, lo que supone elevar la complejidad del problema. Y en segundo lugar, porque es una variable susceptible de poseer multicolinealidad con varias de las demás variables (un modelo concreto de coche pertenece exclusivamente a una única marca, tendrá una potencia CV y Cilindros más o menos similar en cada uno de sus coches, pertenecerá a uno o varios Mercados, pero siempre los mismos, etc). Por ello, es candidata a ser eliminada.

Dado que al dummyficar se generarán demasiadas variables, no es posible estudiar la correlación mediante una matriz como la empleada anteriormente. Sin embargo, se puede emplear un modelo predictivo que nos ayude a ver si posee multicolinealidad con el resto de variables, de forma que, si obtenemos un modelo que predice muy bien el Modelo de coche, podremos afirmar que existe multicolinealidad con una o varias variables.

In [44]:
# Generar variables dummy
df_dummies = pd.get_dummies(df.drop(columns=["Precio"]), drop_first=True)

# Separación de variables
X = df_dummies.drop(columns=[col for col in df_dummies if 'Modelo' in col])
y = df_dummies[[col for col in df_dummies if 'Modelo' in col]]

# Set de train y test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Modelo
model_rf = RandomForestClassifier()
model_rf.fit(X_train, y_train)

# Predicciones y evaluación
y_pred = model_rf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy del Modelo: {accuracy}")
Accuracy del Modelo: 0.8973214285714286

Con un accuracy de 0.9, consideraramos la variable Modelo suficientemente explicada por el resto de variables, lo que supone que podemos eliminarla y evitar una mayor complejidad cuando llegue el momento de crear variables dummy.

In [45]:
df = df.drop(["Modelo"], axis=1)
df.columns
Out[45]:
Index(['Marca', 'Año', 'Combustible', 'CV', 'Cilindros', 'Transmisión',
       'Tracción', 'Mercado', 'Tamaño', 'Estilo', 'Consumo Carretera',
       'Consumo Ciudad', 'Precio'],
      dtype='object')
In [46]:
# Actualizamos las listas de variables
var_list_num_act = []
var_list_categ_act = []

for var in [var for var in var_list_num if (var != "Puertas") & (var != "Popularidad")]:
    var_list_num_act.append(var)
    
for var in [var for var in var_list_categ if var != "Modelo"]:
    var_list_categ_act.append(var)

En el caso de Consumo Carretera y Consumo Ciudad, se decide no eliminarlas a pesar de no tener unas correlaciones elevadas. En cualquier caso, si al finalizar el proyecto el modelo no fuera suficientemente predictivo y se pretendiera volver a iterar, la sugerencia es que se eliminen y se pruebe si no contando con ellas el nuevo modelo predice mejor.

Limpieza de datos:

A continuación, se procede a la solución de valores nulos. Dado que en el caso de las variables Combustible, CV y Cilindros estos valores son muy escasos, se decide una solución sencilla como es la imputación de la moda, es decir, del valor más frecuente de cada variable. Se toma esta decisión debido a que, siendo tan pocos valores nulos, la imputación de la moda no causará una distorsión significativa en los resultados.

In [47]:
for var in ["Combustible", "CV", "Cilindros"]:
    df[var] = impute_mode(df, var)
    
df.isnull().sum()
Out[47]:
Marca                   0
Año                     0
Combustible             0
CV                      0
Cilindros               0
Transmisión             0
Tracción                0
Mercado              3376
Tamaño                  0
Estilo                  0
Consumo Carretera       0
Consumo Ciudad          0
Precio                  0
dtype: int64

Ahora sólo nos quedan valores nulos en la variable Mercado, en torno al 30% de sus valores. Debido a que en este caso sí es un porcentaje muy representativo, imputar la moda como valor en estos casos nulos podría causar una distorsión muy significativa en los resultados. Por ello, se decide realizar una imputación mediante un Random Forest que prediga, en función del resto de variables, el valor de la variable Mercado. La elección de Random Forest se debe a que aún no tenemos los datos completamente preparados y, al no estar normalizados ni las variables categóricas dummyficadas, ésta es la opción más razonable, ya que no tiene problemas para trabajar tanto con variables categóricas como con distintas escalas en los datos cuantitativos.

In [48]:
round(df["Mercado"].isnull().sum()/max(df.count())*100,2)
Out[48]:
30.15
In [49]:
categ_columns = ["Marca", "Combustible", "Transmisión", "Tracción", "Tamaño", "Estilo"]

df_encoded = pd.get_dummies(df, columns=categ_columns, drop_first=True)

# Definir las variables predictoras y la variable objetivo
X = df_encoded.drop(columns=["Mercado"])
y = df_encoded["Mercado"]

# Separar filas con y sin valores nulos en Mercado
df_no_nulos = df_encoded[df_encoded["Mercado"].notna()]
df_nulos = df_encoded[df_encoded["Mercado"].isna()]

# Separar las variables predictoras y la variable objetivo para las filas sin nulos
X_train = df_no_nulos.drop(columns=["Mercado"])
y_train = df_no_nulos["Mercado"]

# Entrenamos el modelo con las filas sin valores nulos
X_train, X_test, y_train, y_test = train_test_split(X_train, y_train, test_size=0.2, random_state=42)

modelo_rf = RandomForestClassifier(n_estimators=100, random_state=42)
modelo_rf.fit(X_train, y_train)

y_pred = modelo_rf.predict(X_test)
print(f"Accuracy en el conjunto de prueba: {round(accuracy_score(y_test, y_pred),4)}")
Accuracy en el conjunto de prueba: 0.9304
In [50]:
# Imputación de valores
X_nulos = df_nulos.drop(columns=["Mercado"]) 
y_nulos_imputados = modelo_rf.predict(X_nulos)

# Asignar los valores imputados a nuestro dataframe
df.loc[df["Mercado"].isna(), "Mercado"] = y_nulos_imputados
In [51]:
df.isnull().sum(), df.count()
Out[51]:
(Marca                0
 Año                  0
 Combustible          0
 CV                   0
 Cilindros            0
 Transmisión          0
 Tracción             0
 Mercado              0
 Tamaño               0
 Estilo               0
 Consumo Carretera    0
 Consumo Ciudad       0
 Precio               0
 dtype: int64,
 Marca                11199
 Año                  11199
 Combustible          11199
 CV                   11199
 Cilindros            11199
 Transmisión          11199
 Tracción             11199
 Mercado              11199
 Tamaño               11199
 Estilo               11199
 Consumo Carretera    11199
 Consumo Ciudad       11199
 Precio               11199
 dtype: int64)

Gracias al Random Forest, ya hemos imputado los valores más probables a cada valor nulo de Mercado, por lo que ya no quedan valores nulos en nuestro dataset.

Antes de continuar al siguiente punto, dado que no pudimos hacerlo anteriormente por la existencia de valores nulos, aprovechamos para eliminar los valores outliers mediante un LOF.

In [52]:
x_lof = df[var_list_num_act]
y_lof = df["Precio"]

n_outliers, outliers_indices = local_outlier_factor(df, x_lof, y_lof)
El número de outliers es de 252 de 11199

El siguiente paso, una vez identificados los outliers, es eliminarlos. Para ello, se resetean los índices (ya que los índices del df original y los de las posiciones de los outliers ya que el df original mantiene sus índices originales pero sus datos han sido movidos debido a la eliminación de observaciones cuando se eliminaron datos duplicados).

In [53]:
df.count()[0]
Out[53]:
11199
In [54]:
# Resetear los índices
df = df.reset_index(drop=True)

# Eliminamos los outliers por índice
df =  df[~df.index.isin(outliers_indices)]
df.count()[0]
Out[54]:
10947

Ahora contamos con un dataframe sin las variables poco repersentativas, sin datos nulos ni duplicados y sin outliers.

Creación de variables:

En principio, se creará una variable Consumo Medio que resultará de la media de Consumo Carretera y Consumo Ciudad, así como se dummyficará las variables categóricas. Ésto último puede dar lugar a un gran número de variables binarias, por lo que se planteará, en ese caso, la posibilidad de la reducción de dimensionalidad.

In [55]:
df["Consumo Medio"] = df[["Consumo Carretera", "Consumo Ciudad"]].mean(axis=1)
df.drop(["Consumo Carretera", "Consumo Ciudad"], axis=1, inplace=True)

A continuación, dado que la variable Mercado ya no tiene valores nulos, y se ha detectado que es una variable categórica que presenta la posibilidad de contener varias categorías en una única observación, parece buena opción separar primero estas categorías y dummyficar después, de forma que, por ejemplo, una observación que tuviera en Mercado "Factory Tuner,Luxury,High-Performance", tendría 1 en las dummies "Factory Tuner", "Luxury" y "High-Performance", y 0 en las demás, en lugar de existir una variable dummy "Factory Tuner,Luxury,High-Performance".

In [56]:
# Separar las categorías de la columna Mercado en una lista dentro de cada celda
df["Mercado_separado"] = df["Mercado"].str.split(',')

# Utilizamos pd.get_dummies() para convertir las listas de categorías en columnas dummy
df_mercado_dummies = df["Mercado_separado"].str.join('|').str.get_dummies(sep='|')

df = pd.concat([df, df_mercado_dummies], axis=1)
df = df.drop(columns=["Mercado", "Mercado_separado"])
df.head()
Out[56]:
Marca Año Combustible CV Cilindros Transmisión Tracción Tamaño Estilo Precio ... Crossover Diesel Exotic Factory Tuner Flex Fuel Hatchback High-Performance Hybrid Luxury Performance
0 BMW 2011 premium unleaded (required) 335.0 6.0 MANUAL rear wheel drive Compact Coupe 46135.0 ... 0 0 0 1 0 0 1 0 1 0
1 BMW 2011 premium unleaded (required) 300.0 6.0 MANUAL rear wheel drive Compact Convertible 40650.0 ... 0 0 0 0 0 0 0 0 1 1
2 BMW 2011 premium unleaded (required) 300.0 6.0 MANUAL rear wheel drive Compact Coupe 36350.0 ... 0 0 0 0 0 0 1 0 1 0
3 BMW 2011 premium unleaded (required) 230.0 6.0 MANUAL rear wheel drive Compact Coupe 29450.0 ... 0 0 0 0 0 0 0 0 1 1
4 BMW 2011 premium unleaded (required) 230.0 6.0 MANUAL rear wheel drive Compact Convertible 34500.0 ... 0 0 0 0 0 0 0 0 1 0

5 rows × 21 columns

Finalmente, aprovechando que tenemos una lista con todas las variables categóricas excepto Mercado (que ya está dummyficada) que empleamos al entrenar el modelo Random Forest, dummyficamos el resto de variables categóricas.

In [57]:
df = pd.get_dummies(df, columns=categ_columns, drop_first=True, dtype="int64")
df.head()
Out[57]:
Año CV Cilindros Precio Consumo Medio Crossover Diesel Exotic Factory Tuner Flex Fuel ... Estilo_Convertible Estilo_Convertible SUV Estilo_Coupe Estilo_Crew Cab Pickup Estilo_Extended Cab Pickup Estilo_Passenger Minivan Estilo_Passenger Van Estilo_Regular Cab Pickup Estilo_Sedan Estilo_Wagon
0 2011 335.0 6.0 46135.0 22.5 0 0 0 1 0 ... 0 0 1 0 0 0 0 0 0 0
1 2011 300.0 6.0 40650.0 23.5 0 0 0 0 0 ... 1 0 0 0 0 0 0 0 0 0
2 2011 300.0 6.0 36350.0 24.0 0 0 0 0 0 ... 0 0 1 0 0 0 0 0 0 0
3 2011 230.0 6.0 29450.0 23.0 0 0 0 0 0 ... 0 0 1 0 0 0 0 0 0 0
4 2011 230.0 6.0 34500.0 23.0 0 0 0 0 0 ... 1 0 0 0 0 0 0 0 0 0

5 rows × 94 columns

Transformación de variables:

El siguiente paso será transformar el resto de variables que lo necesiten para terminar de preparar el dataset. En este caso, se estima necesaria la estandarización de las variables Año, CV, Cilindros y Consumo Medio. En el caso de Año, aunque puede parecer que las fechas y los años no necesitan estandarización, en este caso, y como ya se ha comentado con anterioridad, no se trata de una fecha o año que suponga una secuencia temporal, sino que el año supone un dato para conocer lo antiguo o nuevo que es un coche, por lo que mantenerlo en su escala natural puede hacer que domine el modelo frente al resto de variables estandarizadas.

Por otro lado, dado que el Precio es la variable objetivo, en principio no parece necesario estandarizarla.

En el caso de las variables dummy, no sólo no es necesario, sino que además puede ser una mala idea, ya que podrían perder su significado interpretativo y confundir al modelo.

In [58]:
col_a_estandarizar = ["CV", "Cilindros", "Año", "Consumo Medio"]
standard_dataset(df, col_a_estandarizar)

df
Out[58]:
Año CV Cilindros Precio Consumo Medio Crossover Diesel Exotic Factory Tuner Flex Fuel ... Estilo_Convertible Estilo_Convertible SUV Estilo_Coupe Estilo_Crew Cab Pickup Estilo_Extended Cab Pickup Estilo_Passenger Minivan Estilo_Passenger Van Estilo_Regular Cab Pickup Estilo_Sedan Estilo_Wagon
0 0.032901 0.762786 0.192906 46135.0 -0.049747 0 0 0 1 0 ... 0 0 1 0 0 0 0 0 0 0
1 0.032901 0.439148 0.192906 40650.0 0.102989 0 0 0 0 0 ... 1 0 0 0 0 0 0 0 0 0
2 0.032901 0.439148 0.192906 36350.0 0.179357 0 0 0 0 0 ... 0 0 1 0 0 0 0 0 0 0
3 0.032901 -0.208127 0.192906 29450.0 0.026621 0 0 0 0 0 ... 0 0 1 0 0 0 0 0 0 0
4 0.032901 -0.208127 0.192906 34500.0 0.026621 0 0 0 0 0 ... 1 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
11194 0.172448 0.439148 0.192906 46120.0 -0.507955 1 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
11195 0.172448 0.439148 0.192906 56670.0 -0.507955 1 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
11196 0.172448 0.439148 0.192906 50620.0 -0.507955 1 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
11197 0.311994 0.439148 0.192906 50920.0 -0.507955 1 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
11198 -0.664831 -0.291348 0.192906 28995.0 -0.202483 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 1 0

10947 rows × 94 columns

Reducción de dimensionalidad:

Aunque el método PCA funciona mejor cuando la relación lineal es fuerte (y ya hemos visto que en este caso las relaciones lineales, salvo alguna concreta, son moderadas o débiles), el gran número de variables hace completamente inviable modelar con el dataset actual. Además, debido precisamente a ese gran número de variables, otros métodos, como por ejemplo stepAIC, pueden requerir de una gran cantidad de capacidad computacional y tiempo.

In [59]:
# Separamos las variables predictoras de la objetivo
x = df.drop(["Precio"], axis=1)
y = df["Precio"]
In [60]:
pca_n_components(x)
In [61]:
# Transformamos las variables del dataframe en componentes principales
x = pca_dataframe(x, 27)
x.head(10)
Out[61]:
PC1 PC2 PC3 PC4 PC5 PC6 PC7 PC8 PC9 PC10 ... PC18 PC19 PC20 PC21 PC22 PC23 PC24 PC25 PC26 PC27
0 0.989811 0.218745 1.700410 -0.160140 -1.107791 -0.217034 0.507736 0.159446 -0.131320 0.217536 ... -0.268409 -0.037488 -0.030414 -0.026903 0.077988 -0.004180 -0.174763 0.014092 0.338893 0.612835
1 0.562651 0.158582 1.450570 -0.107588 -0.733525 -0.664724 0.141706 0.205353 0.429161 -0.120974 ... 0.951262 0.114183 -0.094160 0.074335 0.025697 0.123417 -0.222980 0.089899 -0.112924 0.024141
2 0.668222 0.193972 1.608211 -0.204094 -0.980844 -0.237750 0.619761 0.230907 -0.160350 0.162673 ... -0.263575 0.017046 -0.010483 0.001265 0.057728 -0.058325 -0.153766 0.027287 -0.034036 0.061426
3 0.256211 -0.070299 1.325723 -0.118889 -0.759745 -0.711010 0.089270 0.357196 0.461386 -0.192164 ... -0.159464 0.105279 0.056785 0.063935 0.008985 0.029368 -0.202026 -0.009441 0.115149 0.046118
4 0.270108 -0.108762 1.133920 -0.184716 -0.884312 -0.593470 0.501001 0.152908 -0.163352 0.008037 ... 0.790635 0.167340 -0.030596 0.032622 0.076937 0.001653 -0.300702 0.055910 -0.204047 0.135906
5 0.263583 0.042624 1.288008 -0.145350 -0.778620 -0.721627 0.061780 0.364336 0.471845 -0.179718 ... -0.161857 0.110295 0.055958 0.062051 0.004595 0.023773 -0.201230 -0.012203 0.110431 0.053620
6 0.715894 0.152193 1.307947 -0.105070 -0.841299 -0.752735 -0.010750 0.184937 0.466018 -0.118822 ... 0.931621 0.114729 -0.091135 0.065577 0.038049 0.113245 -0.217651 0.083226 -0.130501 0.029282
7 0.675593 0.306895 1.570496 -0.230555 -0.999719 -0.248367 0.592271 0.238047 -0.149891 0.175119 ... -0.265968 0.022062 -0.011311 -0.000620 0.053337 -0.063920 -0.152969 0.024526 -0.038754 0.068927
8 0.277480 0.004161 1.096205 -0.211178 -0.903187 -0.604087 0.473512 0.160048 -0.152893 0.020483 ... 0.788242 0.172356 -0.031423 0.030738 0.072547 -0.003942 -0.299906 0.053149 -0.208766 0.143407
9 0.321320 0.087256 1.032263 -0.230394 -0.944287 -0.634053 0.414780 0.160299 -0.135835 0.030355 ... 0.781537 0.176254 -0.031287 0.027135 0.072342 -0.010681 -0.297976 0.049409 -0.216699 0.150318

10 rows × 27 columns

Ya tenemos preparado el dataframe de componentes principales que servirá como base para el entrenamiento y testeo del modelo. Hay que recalcar que, aunque 27 componentes sigue siendo un número considerable de variables, no son las 94 que teníamos al terminar de dummyficar las variables categóricas, ni las más de 900 que tendríamos en caso de haber mantenido la variable Mercado.

Al existir tantas categorías distintas entre todas las variables, es probable que cualquier modelo que se ajuste con estos datos no resulte especialmente preciso a la hora de estimar el precio. En caso de ocurrir algo así, se recomendará volver a este punto de Preparación de los datos (concretamente al apartado de Creación de Variables) y retomar el estudio desde aquí, seleccionando sólo las variables con mayor poder de predicción del Precio. Por ejemplo, en la matriz de correlaciones se pudo ver que Año, CV y Cilindros eran las que tenían una mayor correlación con Precio (sobre todo las dos últimas), por lo que podrían realizarse modelos en base sólo a estas variables, y, a lo sumo, incorporar alguna de las variables categóricas con menor número de valores únicos para no aumentar la complejidad.

En cualquier caso, dado que desconocemos por completo cómo se comportarán los modelos que entrenemos con estos datos, continuaremos adelante con los 27 componentes principales que hemos obtenido.

4. Desarrollo del modelo:¶

Antes de desarrollar un modelo predictivo es necesario seleccionar qué modelo se va a implementar, en función de las características del set de datos. Por un lado, tenemos un dataframe formado 27 componentes provenientes de unas variables con relaciones lineales débiles o moderadas con la variable target, pero con relaciones no necesariamente lineales algo más fuertes. Y del otro lado, tenemos una variable target continua que no se ha discretizado en parte por el fuerte sesgo de su distribución, y en parte, para no perder variabilidad de estos datos y, por tanto, precisión en el modelo.

Por ello, los modelos que parecen que podrían funcionar mejor son: XGBoost, Random Forest y Regresión de Ridge.

  • XGBoost tiene una buena capacidad para captar relaciones no lineales y es robusto ante el ruido en los datos, se ajusta bien a datos con muchas variables y características complejas, y ofrece regularización para evitar el sobreajuste. También tiene sus desventajas, como que puede ser lento de entrenar cuando existen muchos datos y puede requerir del ajuste de muchos hiperparámetros para obtener los mejores resultados.
  • Random Forest se adapta generalmente bien a los problemas de predicción de precios, evita el sobreajuste bastante bien, es un modelo no paramétrico y funciona bien con conjuntos de datos con alta dimensionalidad. Su principal desventaja es la dificultad para interpretar sus resultados.
  • Regresión de Ridge es una regeresión lineal con penalización L2, simple de entrenar en comparación con los modelos de ensamble, que puede ser muy eficaz cuando hay muchas variables linealmente independientes. Su principal desventaja es que asume la linealidad entre las variables predictoras, pero puede ser útil como modelo base.
In [62]:
x = x.reset_index(drop=True)
y = y.reset_index(drop=True)

# División de los datos en training y test
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42)
In [63]:
ridge_reg_alpha1, ridge_reg_alpha1_metrics = ridge_regression_fit(x_train, x_test, y_train, y_test)
RMSE de Ridge Regression: 26450.95
R2 score de Ridge Regression: 0.73
In [64]:
ridge_reg_alpha05, ridge_reg_alpha05_metrics = ridge_regression_fit(x_train, x_test, y_train, y_test, _alpha=.5)
RMSE de Ridge Regression: 26450.47
R2 score de Ridge Regression: 0.73
In [65]:
ridge_reg_alpha015, ridge_reg_alpha015_metrics = ridge_regression_fit(x_train, x_test, y_train, y_test, _alpha=.15)
RMSE de Ridge Regression: 26450.13
R2 score de Ridge Regression: 0.73
In [66]:
rand_for, rand_for_metrics = random_forest_regression_fit(x_train, x_test, y_train, y_test)
RMSE de Random Forest: 11793.6
R2 score de Random Forest: 0.95
In [67]:
xgboost, xgboost_metrics = xgboost_fit(x_train, x_test, y_train, y_test)
RMSE de XGBoost: 11238.07
R2 score de XGBoost: 0.95

Regresión de Ridge:

En el caso de la regresión de Ridge, dado que su entrenamiento es más rápido que en los modelos ensamble, se han entrenado 3 modelos modificando el valor de alpha, aunque se puede comprobar por la raiz del error cuadrático medio y por el coeficiente de determinación idénticos en los 3 casos que es indiferente el alpha que se utilice.

Dado que es un modelo eminentemente lineal, su R2 de 0.73 implica que explica hasta el 73% de la variabilidad del Precio, un porcentaje no muy bajo pero superado por los otros dos modelos.

Random Forest:

El modelo de Random Forest tiene un R2 mucho mayor y una raíz del error cuadrático medio muy inferior, lo que implica que captura mucho mejor las relaciones entre las variables predictoras y el precio.

XGBoost:

Presenta los mejores valores de RSME y R2, pues explica el 95% de la variabilidad del precio y su error cuadrático medio es el más bajo. Es la mejor opción de las 3 seleccionadas, pero, aún así, se emplearán otras métricas para evaluarlo y se comparará con el modelo de Random Forest, ya que éste obtuvo unos resultados casi tan buenos como el modelo XGBoost.

5. Evaluación del modelo¶

Ya se ha visto que el modelo XGBoost era el mejor modelo según RMSE y R2, pero Random Forest era casi tan bueno. Por ello, aunque los modelos han sido evaluados justo tras ser entrenados, se calculará el R2 ajustado de ambos y se estudiarán los residuos para determinar si XGBoost es verdaderamente mejor modelo para nuestro caso que Random Forest.

R2 ajustado:

In [68]:
n = len(y_test)
p = x_test.shape[1]

# R2 ajustado de ambos modelos
r2_adj_rand_for = round(adjusted_r2(rand_for_metrics["R2"], n, p),2)
r2_adj_xgboost = round(adjusted_r2(xgboost_metrics["R2"], n, p),2)

print(f"R2 ajustado de Random Forest: {r2_adj_rand_for}")
print(f"R2 ajustado de XGBoost: {r2_adj_xgboost}")
R2 ajustado de Random Forest: 0.95
R2 ajustado de XGBoost: 0.95

Residuos:

In [69]:
rand_for_pred = rand_for.predict(x_test)
xgboost_pred = xgboost.predict(x_test)

rand_for_residuals = y_test - rand_for_pred
xgboost_residuals = y_test - xgboost_pred

Residuos vs Predicciones:

In [70]:
residuals_vs_predictions(rand_for_pred, rand_for_residuals)
In [71]:
residuals_vs_predictions(xgboost_pred, xgboost_residuals)

Datos de test vs Estimaciones:

In [72]:
test_vs_estimacion(y_test, rand_for_pred)
In [73]:
test_vs_estimacion(y_test, xgboost_pred)

Distribuciones de los residuos:

In [74]:
residual_histogram(rand_for_residuals, 400)
In [75]:
residual_histogram(xgboost_residuals, 400)

Gráficos QQPlot para comprobar la normalidad de los residuos:

In [76]:
residuals_qqplot(rand_for_residuals)
In [77]:
residuals_qqplot(xgboost_residuals)

A la vista de los residuos, se puede confirmar que tanto el modelo Random Forest como XGBoost son modelos que predicen bien el Precio, dado que los residuos se comportan siguiendo una distribución normal.

Por otro lado, el R2 ajustado, que penaliza las variables poco explicativas, ha arrojado en los dos modelos el mismo valor que para sus R2, lo que implica que sus R2 no están infladas. Esto es un indicativo de que los modelos no están sobreajustados.

La conclusión final es que el modelo XGBoost que se ha entrenado es la mejor opción y cuenta con el respaldo de un R2 de 0.95 y una distribución normal de los residuos.

6. Despliegue del modelo¶

A la hora de desplegar el modelo, hay que recuperar todas las transformaciones realizadas a los datos, desde la eliminación de outliers y variables no predictoras hasta la transformación de variables en componentes principales, y generar una función de Python que prepare los nuevos datos no etiquetados para poder pasarlos al modelo y predecir los precios.

Se propondrá una exposición narrativa del proyecto en archivo adjunto.

In [ ]: